Embedding in JAVA
contents
클린 코드(Clean Code) 를 작성하고 데이터베이스 엔티티 설계에서도 객체 지향 원칙을 지키기 위한 아주 훌륭한 기능인 @Embedded와 @Embeddable에 대해 알아보겠습니다.
보통 초보적인 데이터베이스 설계를 보면 User 테이블 하나에 컬럼이 50개씩 있는 경우가 있습니다. 그중 10개는 "집 주소"(도로명, 시, 우편번호...)이고, 또 다른 10개는 "직장 주소"인 식이죠.
@Embeddable과 @Embedded를 사용하면, 이렇게 연관된 필드들을 하나의 재사용 가능한 자바 클래스로 묶으면서도, 데이터베이스상에서는 여전히 하나의 테이블로 유지할 수 있습니다.
1. 핵심 개념: "값 타입 (Value Types)"
도메인 주도 설계(DDD)에서는 엔티티(Entity)와 값 객체(Value Object)를 구분합니다.
- 엔티티 (Entity): 식별자(ID)를 가집니다. 고유한 정체성이 중요합니다 (예: 사용자).
- 값 객체 (Value Object): 식별자가 없습니다. 오직 속성들의 값으로만 정의됩니다 (예: 주소, 금액, 날짜 구간).
JPA는 이러한 값 객체를 매핑하기 위해 다음 두 애노테이션을 사용합니다.
@Embeddable: 임베딩될 클래스(예:Address) 위에 붙입니다. "나는 독자적인 테이블이 아니라, 다른 테이블의 부속품입니다"라고 선언하는 것입니다.@Embedded: 부모 엔티티(예:User) 내부의 필드 위에 붙입니다. "이 클래스의 필드들을 가져와서 내 테이블에 평평하게(Flatten) 펼쳐 넣으세요"라고 명령하는 것입니다.
2. 매핑 구조 시각화
여기서의 마법은 자바에서는 클래스가 두 개지만, 데이터베이스에는 오직 하나의 테이블만 존재한다는 점입니다.
- Java:
User객체 안에Address객체가 필드로 들어있습니다. - Database:
USER테이블 하나에name,street,city,zip컬럼이 모두 들어있습니다.ADDRESS테이블은 존재하지 않습니다.
3. 단계별 구현 방법
1단계: 임베더블 클래스 만들기 (@Embeddable)
이 클래스에는 @Entity나 @Id가 없습니다.
import jakarta.persistence.Embeddable;
@Embeddable // 전체가 아닌 '부품'임을 표시
public class Address {
private String street;
private String city;
private String zipCode;
// Getter, Setter, 기본 생성자 필수
}
2단계: 부모 엔티티에서 사용하기 (@Embedded)
import jakarta.persistence.Entity;
import jakarta.persistence.Embedded;
import jakarta.persistence.Id;
@Entity
public class User {
@Id
private Long id;
private String name;
@Embedded // Address의 컬럼들을 이 테이블에 끼워 넣음
private Address homeAddress;
}
결과로 생성되는 SQL 테이블 (User):
| id | name | street | city | zip_code |
|---|---|---|---|---|
| 1 | 철수 | 테헤란로 123 | 서울 | 12345 |
4. "컬럼명 중복" 문제 (@AttributeOverrides)
만약 사용자에게 homeAddress(집 주소)와 workAddress(직장 주소), 이렇게 두 개의 주소가 있다면 어떻게 될까요?
단순히 @Embedded 필드를 두 개 넣으면, 하이버네이트는 street, city, zip_code라는 이름의 컬럼을 한 테이블에 두 번 만들려고 시도합니다. 당연히 컬럼 이름 중복 에러(Duplicate Column Name Error) 가 발생합니다.
해결책: 둘 중 적어도 하나의 컬럼 이름들을 재정의(Override)해줘야 합니다.
@Entity
public class User {
@Id
private Long id;
// 기본 컬럼명 사용 (street, city, zip_code)
@Embedded
private Address homeAddress;
// 컬럼명 재정의 (work_street, work_city, work_zip)
@Embedded
@AttributeOverrides({
@AttributeOverride(name = "street", column = @Column(name = "work_street")),
@AttributeOverride(name = "city", column = @Column(name = "work_city")),
@AttributeOverride(name = "zipCode", column = @Column(name = "work_zip"))
})
private Address workAddress;
}
5. @Embedded vs. @OneToOne
면접에서 아주 자주 나오는 질문입니다. 둘 다 코드를 두 개의 클래스로 나눌 수 있게 해주는데, 차이점이 뭘까요?
| 특징 | @Embedded | @OneToOne |
|---|---|---|
| DB 구조 | 1개의 테이블 (컬럼이 합쳐짐). | 2개의 테이블 (외래 키로 조인). |
| 성능 | 더 빠름 (조인(JOIN) 불필요). | 상대적으로 느림 (조인 필요). |
| 정체성(Identity) | 별도 ID 없음. 부모가 죽으면 같이 삭제됨. | 별도 ID 있음. 독립적으로 존재 가능. |
| Null 처리 | 모든 필드가 null이면 객체 자체가 null이 됨. | 객체 자체가 null일 수 있음. |
| 재사용 | 여러 User가 하나의 Address 객체를 공유 불가. | 공유 가능 (두 유저가 한 주소를 가리킴). |
6. 언제 무엇을 써야 할까요?
@Embedded사용: 부모에게 완전히 종속되어 있으며, 그 자체로는 별 의미가 없는 데이터.- 예시:
Address(주소),Money(통화+금액),DateRange(시작일+종료일),GPSCoordinates(위도+경도).
- 예시:
@OneToOne사용: 서로 관련은 있지만 독립적으로 관리되어야 하는 별개의 엔티티.- 예시:
User와Profile,Car와Engine(만약 엔진을 재고 목록에서 따로 관리한다면).
- 예시:
개발자를 위한 요약
- 더 깔끔한 코드:
User클래스가 100개의 필드를 가진 "신(God Class)"이 되는 것을 막아줍니다. - 재사용성:
Address클래스를 한 번만 잘 만들어두면User,Company,Order등 모든 엔티티에서 필드를 다시 작성할 필요 없이 재사용할 수 있습니다. - 성능 비용 없음: DB 입장에서는 어차피 하나의 테이블이므로, 필드를 엔티티에 직접 적는 것과 성능 차이가 전혀 없습니다.
references